package fr.babos.touch.emulator 
{
	import flash.display.DisplayObject;
	import flash.display.Stage;
	import flash.events.Event;
	import flash.events.TouchEvent;
	import flash.geom.Point;
	import flash.utils.Dictionary;
	import fr.babos.core.roles.IDisposable;
	import fr.babos.touch.delegate.IGestureTouchDelegate;
	import fr.babos.touch.errors.TouchError;
	import fr.babos.touch.events.GestureTouchDragEvent;
	import fr.babos.touch.events.GestureTouchEvent;
	import fr.babos.touch.events.GestureTouchSwipeEvent;
	import fr.babos.touch.events.GestureTouchTapEvent;
	import fr.babos.touch.events.GestureTouchTransformEvent;
	import fr.babos.utils.ArrayUtils;
	import fr.babos.utils.ScreenUtils;
	
	/**
	 * @author ZoulouX
	 */
	public class GestureTouchEmulator implements IDisposable
	{
		/**
		 * Les types d'events pris en charge par le delegate
		 */
		public static const EVENT_TYPES					:Vector.<String> = Vector.<String>([
			GestureTouchDragEvent.START_DRAG,
			GestureTouchDragEvent.STOP_DRAG,
			GestureTouchDragEvent.DRAGGING,
			GestureTouchDragEvent.SINGLE_DRAGGING,
			GestureTouchDragEvent.MULTI_DRAGGING,
			GestureTouchDragEvent.LOCK_DRAG,
			GestureTouchDragEvent.UNLOCK_DRAG,
			
			GestureTouchSwipeEvent.SWIPE,
			
			GestureTouchTapEvent.SINGLE_TAP,
			GestureTouchTapEvent.MULTI_TAP,
			GestureTouchTapEvent.SINGLE_DOUBLE_TAP,
			GestureTouchTapEvent.MULTI_DOUBLE_TAP,
			
			GestureTouchTransformEvent.PINCH_START,
			GestureTouchTransformEvent.PINCHING,
			GestureTouchTransformEvent.PINCH_END
		]);
		
		/**
		 * La liste des émulateurs attachés au containers
		 */
		protected static const __emulators				:Dictionary					= new Dictionary(false);
		
		
		/**
		 * La liste des émulateurs attachés au containers
		 */
		static public function get emulators ():Dictionary { return __emulators; }
		
		
		/**
		 * Emuler des GestureTouchEvent sur un container. Si ce container implémente IGestureTouchDelegate, les events seront mappés automatiquement sur gestureTouchHandler.
		 * @param	pTarget : Le container en question, peut implémenter IGestureTouchDelegate
		 * @param	pDelegate : Le delegate de cette emulation (optionnel).
		 * @return L'émulateur
		 */
		public static function emulate (pTarget:DisplayObject, pDelegate:IGestureTouchDelegate = null):GestureTouchEmulator
		{
			// Créer / retourner
			return new GestureTouchEmulator(pTarget, pDelegate);
		}
		
		/**
		 * Détacher un émulateur
		 * @param	pTarget : Le container sur lequel on veut arrêter l'émulation
		 */
		public static function demulate (pTarget:DisplayObject):void
		{
			// Si on a un émulateur sur ce container
			if (isEmulated(pTarget))
			{
				// On récupère
				var emulator:IDisposable = (__emulators[pTarget] as IDisposable);
				
				// On le supprime du dico
				__emulators[pTarget] = null;
				delete __emulators[pTarget];
				
				// On le dispose
				emulator.dispose();
			}
		}
		
		/**
		 * Si un container est émulé.
		 * @return True si ce container a un émulateur
		 */
		public static function isEmulated (pTarget:DisplayObject):Boolean
		{
			return __emulators[pTarget] != null;
		}
		
		
		
		/**
		 * Le container de cet emulateur
		 */
		protected var _rootTarget						:DisplayObject;
		
		/**
		 * Le stage du container
		 */
		protected var _rootStage						:Stage;
		
		/**
		 * Les ids des points
		 */
		protected var _pointsIds						:Array						= [];
		
		/**
		 * Les events
		 */
		protected var _events							:Array						= [];
		
		/**
		 * Le dernier event de déplacement
		 */
		protected var _dragEvent						:GestureTouchDragEvent;
		
		/**
		 * Le nombre de points en déplacement
		 */
		protected var _draggingPoints					:uint;
		
		/**
		 * Les points touch détéctés sur ce container
		 */
		protected var _positions						:Array						= [];
		
		protected var _movingPositions					:Array						= [];
		
		/**
		 * Les directions
		 */
		protected var _directions						:Array						= [];
		
		/**
		 * Les deltas
		 */
		protected var _deltas							:Array						= [];
		
		/**
		 * Le nombre de points
		 */
		protected var _totalPoints						:uint						= 0;
		
		/**
		 * Une instance de point pour faire des calculs
		 */
		protected var _currentPoint						:Point 						= new Point();
		
		/**
		 * Le delegate de cet émulateur
		 */
		protected var _delegate							:IGestureTouchDelegate;
		
		/**
		 * Le multiplicateur pour la détéction de la direction
		 */
		protected var _directionDetectionMultiplier		:Number						= 2;
		
		/**
		 * Le ratio de ce stage
		 */
		protected var _stageRatio						:Number;
		
		
		
		/**
		 * Le delegate de cet émulateur
		 */
		public function get delegate ():IGestureTouchDelegate { return _delegate; }
		public function set delegate (value:IGestureTouchDelegate):void
		{
			var eventType:String;
			
			// Si on a un ancien delegate
			if (_delegate != null && _rootTarget != null)
			{
				// On lui détache tous les events pris en charge
				for each (eventType in EVENT_TYPES)
				{
					_rootTarget.removeEventListener(eventType, _delegate.gestureTouchHandler);
				}
			}
			
			// Enregistrer ce delegate
			_delegate = value;
			
			// Si on a un nouveau delegate
			if (_delegate != null)
			{
				// On lui attache tous les events pris en charge
				for each (eventType in EVENT_TYPES)
				{
					_rootTarget.addEventListener(eventType, _delegate.gestureTouchHandler);
				}
			}
		}
		
		/**
		 * Le multiplicateur pour la détéction de la direction
		 */
		public function get directionDetectionMultiplier ():Number { return _directionDetectionMultiplier; }
		public function set directionDetectionMultiplier (value:Number):void 
		{
			_directionDetectionMultiplier = value;
		}
		
		/**
		 * Le clip cible de l'émulateur
		 */
		public function get target ():DisplayObject { return _rootTarget;}
		
		/**
		 * Le nombre de points détéctés
		 */
		public function get totalPoints ():uint { return _totalPoints; }
		
		/**
		 * Les ID's des points détéctés
		 */
		public function get pointsIds ():Array { return _pointsIds; }
		
		
		/**
		 * Le constructeur
		 * @param	pTarget : Le container sur lequel on veut émuler les GestureTouchEvent. Peut implémenter IGestureTouchDelegate.
		 * @param	pDelegate : Le delegate de cette emulation (optionnel).
		 */
		public function GestureTouchEmulator (pTarget:DisplayObject, pDelegate:IGestureTouchDelegate = null)
		{
			// Vérifier la validité du container
			if (pTarget == null)
			{
				throw new TouchError("GestureTouchEmulator.construct", "pTarget can't be null in GestureTouchEmulator instanciation.");
				return;
			}
			
			// Vérifier que ce container soit bien ajouté au stage
			if (pTarget.stage == null)
			{
				throw new TouchError("GestureTouchEmulator.construct", "pTarget.stage can't be null in GestureTouchEmulator instanciation. Please add pTarget to stage.");
				return;
			}
			
			// Désactiver l'ancien émulateur s'il y en avait un
			demulate(pTarget);
			
			// Ajouter ce nouvel émulateur au dico
			__emulators[pTarget] = this;
			
			// Enregistrer le container et son stage
			_rootTarget = pTarget;
			_rootStage = pTarget.stage;
			
			// Récuéprer le ratio de ce stage
			_stageRatio = ScreenUtils.getRatioForStage(_rootStage);
			
			// Ecouter les touch events sur le container et son stage
			//_rootTarget.addEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler, false, 100, true);
			_rootTarget.addEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler, false);
			
			// Si on a un delegate
			if (pDelegate != null)
			{
				// On l'enregistre
				delegate = pDelegate;
			}
			// Sinon vérifier si le container peut être un delegate
			else if (pTarget is IGestureTouchDelegate)
			{
				// On enregistre le container comme étant le delegate
				delegate = pTarget as IGestureTouchDelegate;
			}
		}
		
		/**
		 * Début d'un touch
		 */
		protected function touchBeginHandler (event:TouchEvent):void 
		{
			// Enregistrer ce point (event / position / delta)
			_events[event.touchPointID]				= event;
			_positions[event.touchPointID] 			= new Point(event.stageX / _stageRatio, event.stageY / _stageRatio);
			//_movingPositions[event.touchPointID] 	= _positions[event.touchPointID];
			_deltas[event.touchPointID] 			= new Point(0, 0);
			
			// Enregistrer l'id de ce point
			_pointsIds[_totalPoints] = event.touchPointID;
			
			// Compter le nombre de points
			_totalPoints ++;
			
			// Si c'est le premier point ajouté
			if (_totalPoints == 1)
			{
				// On active l'écoute des moves et des ends
				_rootStage.addEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler);
				//_rootStage.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
				_rootStage.addEventListener(TouchEvent.TOUCH_END, touchEndHandler);
				
				// Dispatcher l'annulation
				dispatchDragEvent(GestureTouchDragEvent.LOCK_DRAG);
			}
		}
		
		/**
		 * Fin d'un touch
		 */
		protected function touchEndHandler (event:TouchEvent):void 
		{
			if (_positions == null)
			{
				trace("GESTURE TOUCH EMULATOR PROBLEM (touchEnd)");
				return;
			}
			
			// Vérifier qu'on soit bien en phase montante ou sur la cible
			// Et qu'il y a bien un point enregistré avec cet ID sur ce container
			if (event.touchPointID in _positions)
			{
				// Si on avait une direction
				if (event.touchPointID in _directions)
				{
					// On a un point de moins qui se déplace
					_draggingPoints --;
					
					// Dispatcher l'arrêt du drag si on a plus de point en mouvement
					if (_draggingPoints == 0)
						dispatchDragEvent(GestureTouchDragEvent.STOP_DRAG);
				}
				else if (_totalPoints == 1)
				{
					// Dispatcher la fin de l'annulation du drag
					dispatchDragEvent(GestureTouchDragEvent.UNLOCK_DRAG);
				}
				
				// Supprimer l'id de ce point
				_pointsIds = ArrayUtils.deleteElement(_pointsIds, event.touchPointID);
				
				// Supprimer les events
				_events[event.touchPointID] = null;
				delete _events[event.touchPointID];
				
				// Supprimer le point
				_positions[event.touchPointID] = null;
				delete _positions[event.touchPointID];
				
				// Supprimer le delta du point
				_deltas[event.touchPointID] = null;
				delete _deltas[event.touchPointID];
				
				// Supprimer la direction du point
				_directions[event.touchPointID] = null;
				delete _directions[event.touchPointID];
				
				// Décompter ce point
				_totalPoints --;
				
				// Si c'est le dernier point ajouté
				if (_totalPoints == 0)
				{
					// On désactive l'écoute des moves et des ends
					_rootStage.removeEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler);
					//_rootStage.removeEventListener(Event.ENTER_FRAME, enterFrameHandler);
					_rootStage.removeEventListener(TouchEvent.TOUCH_END, touchEndHandler);
				}
			}
		}
		
		/**
		 * Un point touch bouge
		 */
		protected function touchMoveHandler (event:TouchEvent):void 
		{
			if (_positions == null)
			{
				trace("GESTURE TOUCH EMULATOR PROBLEM (touchMove)");
				return;
			}
			
			// ENTER_FRAME SYSTEM
			/*
			_events[event.touchPointID] = event;
			
			// La position globale du point
			_currentPoint.x = event.stageX / _stageRatio;
			_currentPoint.y = event.stageY / _stageRatio;
			
			_movingPositions[event.touchPointID] = _currentPoint.clone();
			
			// Calculer les deltas avec l'ancienne position
			//_deltas[event.touchPointID].x = _currentPoint.x - _positions[event.touchPointID].x;
			//_deltas[event.touchPointID].y = _currentPoint.y - _positions[event.touchPointID].y;
			*/
			// TOUCH_MOVE SYSTEM
			
			// Vérifier qu'on soit bien en phase montante ou sur la cible
			// Et qu'il y a bien un point enregistré avec cet ID sur ce container
			//if ((event.eventPhase == EventPhase.CAPTURING_PHASE || event.eventPhase == EventPhase.AT_TARGET) && event.touchPointID in _positions)
			if (event.touchPointID in _positions)
			{
				// Actualiser l'event
				_events[event.touchPointID] = event;
				
				// La position globale du point
				_currentPoint.x = event.stageX / _stageRatio;
				_currentPoint.y = event.stageY / _stageRatio;
				
				// Calculer les deltas avec l'ancienne position
				_deltas[event.touchPointID].x = _currentPoint.x - _positions[event.touchPointID].x;
				_deltas[event.touchPointID].y = _currentPoint.y - _positions[event.touchPointID].y;
				
				// Enregistrer la nouvelle position du point
				_positions[event.touchPointID] = _currentPoint.clone();
				
				// Si ce point n'a pas de direction
				if (!(event.touchPointID in _directions))
				{
					// Si on déplace significativement plus en X qu'en Y
					if (Math.abs(_deltas[event.touchPointID].x) > Math.abs(_deltas[event.touchPointID].y) * _directionDetectionMultiplier)
					{
						// On est en déplacement horizontal
						_directions[event.touchPointID] = 0;
					}
					
					// Si on déplace significativement plus en Y qu'en X
					else if (Math.abs(_deltas[event.touchPointID].y) > Math.abs(_deltas[event.touchPointID].x) * _directionDetectionMultiplier)
					{
						// On est en déplacement vertical
						_directions[event.touchPointID] = 1;
					}
					
					// Sinon
					else
					{
						// On est en déplacement libre
						_directions[event.touchPointID] = 2
					}
					
					// On a un point de plus qui se déplace
					_draggingPoints ++;
					
					// On doit dispatcher un start drag si c'est le premier point détécté
					if (_draggingPoints == 1)
						dispatchDragEvent(GestureTouchDragEvent.START_DRAG);
				}
				else
				{
					// On dispatche le dragging
					dispatchDragEvent(GestureTouchDragEvent.DRAGGING);
					
					// On doit aussi dispatcher un dragging simple ou multiple
					if (_draggingPoints == 1)
						dispatchDragEvent(GestureTouchDragEvent.SINGLE_DRAGGING);
					else if (_draggingPoints == 2)
						dispatchDragEvent(GestureTouchDragEvent.MULTI_DRAGGING);
				}
				
			}
			
		}
		
		/**
		 * Boucle par frame pour les dispatchs
		 */
		protected function enterFrameHandler (e:Event):void
		{
			for each (var event:TouchEvent in _events)
			{
				// La position globale du point
				//_currentPoint.x = event.stageX / _stageRatio;
				//_currentPoint.y = event.stageY / _stageRatio;
				
				// Calculer les deltas avec l'ancienne position
				_deltas[event.touchPointID].x = _movingPositions[event.touchPointID].x - _positions[event.touchPointID].x;
				_deltas[event.touchPointID].y = _movingPositions[event.touchPointID].y - _positions[event.touchPointID].y;
				
				// Enregistrer la nouvelle position du point
				_positions[event.touchPointID] = _movingPositions[event.touchPointID].clone();
				
				// Si ce point n'a pas de direction
				if (!(event.touchPointID in _directions))
				{
					// Si on déplace significativement plus en X qu'en Y
					if (Math.abs(_deltas[event.touchPointID].x) > Math.abs(_deltas[event.touchPointID].y) * _directionDetectionMultiplier)
					{
						// On est en déplacement horizontal
						_directions[event.touchPointID] = 0;
					}
					
					// Si on déplace significativement plus en Y qu'en X
					else if (Math.abs(_deltas[event.touchPointID].y) > Math.abs(_deltas[event.touchPointID].x) * _directionDetectionMultiplier)
					{
						// On est en déplacement vertical
						_directions[event.touchPointID] = 1;
					}
					
					// Sinon
					else
					{
						// On est en déplacement libre
						_directions[event.touchPointID] = 2
					}
					
					// On a un point de plus qui se déplace
					_draggingPoints ++;
					
					// On doit dispatcher un start drag si c'est le premier point détécté
					if (_draggingPoints == 1)
						dispatchDragEvent(GestureTouchDragEvent.START_DRAG);
				}
				else
				{
					// On dispatche le dragging
					dispatchDragEvent(GestureTouchDragEvent.DRAGGING);
					
					// On doit aussi dispatcher un dragging simple ou multiple
					if (_draggingPoints == 1)
						dispatchDragEvent(GestureTouchDragEvent.SINGLE_DRAGGING);
					else if (_draggingPoints == 2)
						dispatchDragEvent(GestureTouchDragEvent.MULTI_DRAGGING);
				}
			}
		}
		
		/**
		 * Dispatcher un GestureTouchEvent
		 */
		protected function dispatchGestureTouchEvent (event:GestureTouchEvent):void
		{
			// Puis on dispatche sur la cible
			_rootTarget.dispatchEvent(event);
		}
		
		/**
		 * Dispatcher un event de Drag
		 * @param	pType : Le type d'event de drag
		 */
		protected function dispatchDragEvent (pType:String):void
		{
			// Si on a moins de 3 points
			if (_totalPoints < 3)
			{
				// Créer l'event
				_dragEvent = new GestureTouchDragEvent(pType);
				
				// La direction du point
				if (_directions[_pointsIds[0]] == 0)
					_dragEvent.isHorizontal = true;
				else if (_directions[_pointsIds[0]] == 1)
					_dragEvent.isVertical = true;
				else
					_dragEvent.isBoth = true;
				
				// Si on a un seul point
				if (_totalPoints == 1)
				{
					// La position du seul point
					_dragEvent.position = _positions[_pointsIds[0]];
					
					// Le delta du seul point
					_dragEvent.xDelta = _deltas[_pointsIds[0]].x;
					_dragEvent.yDelta = _deltas[_pointsIds[0]].y;
				}
				
				// Si on a deux points
				else if (_totalPoints == 2)
				{
					// La position interpolée des 2 points
					_dragEvent.position = Point.interpolate(
						_positions[_pointsIds[0]],
						_positions[_pointsIds[1]],
						.5
					);
					
					// Le delta interpolé des 2 points
					var deltaPoint:Point = Point.interpolate(
						_deltas[_pointsIds[0]],
						_deltas[_pointsIds[1]],
						.5
					);
					
					// Appliquer le delta sur l'event
					_dragEvent.xDelta = deltaPoint.x / 2;
					_dragEvent.yDelta = deltaPoint.y / 2;
				}
				
				// Ajouter tous les touchEvents a cet event
				_dragEvent.touchEvents = Vector.<TouchEvent>(_events);
				
				// Dispatcher l'event sur le clip émulé
				dispatchGestureTouchEvent(_dragEvent);
			}
		}
		
		
		/**
		 * Supprimer proprement cet émulateur
		 */
		public function dispose ():void
		{
			// Ne plus écouter les events sur le container et son stage
			_rootTarget.removeEventListener(TouchEvent.TOUCH_BEGIN, touchBeginHandler);
			_rootStage.removeEventListener(TouchEvent.TOUCH_END, touchEndHandler);
			_rootStage.removeEventListener(TouchEvent.TOUCH_MOVE, touchMoveHandler);
			
			// Virer du dico si ce n'est pas déjà fait
			demulate(_rootTarget);
			
			// Supprimer les références
			_rootTarget = null;
			_rootStage = null;
			_positions = null;
			delegate = null;
			_dragEvent = null;
		}
	}
}